Skip to content

fix(libkernel): fail unlink, remove, rename and open of a file named with a trailing slash with ENOTDIR on Windows - #8680

Closed
xXPBSXx wants to merge 2 commits into
boykopovar:mainfrom
xXPBSXx:fix/trailing-slash-enotdir
Closed

xXPBSXx wants to merge 2 commits into
boykopovar:mainfrom
xXPBSXx:fix/trailing-slash-enotdir

Conversation

@xXPBSXx

@xXPBSXx xXPBSXx commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

What

A name that ends in / must be a directory. FreeBSD 11.4 fails the lookup with ENOTDIR otherwise (vfs_cache_lookup, sys/kern/vfs_cache.c:2104-2105). On Windows ResolveAlias dropped the slash for paths under a mount alias (it kept it only off Windows, #4681), and unlink/sceKernelUnlink, remove, rename and open/sceKernelOpen never asked whether a name with a slash is a non-directory, so they failed with ENOENT, EINVAL or EIO. ResolveAlias now keeps the slash on every host, and a shared helper (NamesNonDirectoryWithSlash_nid_no_patch) makes the four calls answer ENOTDIR (the source, and an existing source's destination, in rename). The trailing-slash block of guest_filesystem, which was compiled only off Windows, now runs on Windows.
Fixes #8679

Scope note: this changes the guest path resolver (General.cpp, ResolveAlias) and the kernel-style lookup of unlink, remove, rename and open, not the stdio/printf formatting that the console implements with Dinkumware; remove and rename follow the kernel lookup like the merged #8200 and #8199. git merge-tree against upstream/main, #8626, #8522, #8603, #8663, #8312 and #5528 is clean; pr_overlap was not run.

Tested

Windows 10 (19045), MinGW-w64 GCC 15.2.0, Release, -j4, on upstream/main 2674007 + this commit:

  • guest_filesystem: fails at the first trailing-slash check (guest errno 2) with the code taken back to main and the block enabled, passes with the change (3 of 3 runs).
  • The tests whose sources call the path resolver, unlink, remove, rename or open (65 test targets, 87 ctest entries): 85 pass, 2 were not run (guest_memory_pool_stats, guest_fstatvfs_conversion; their executables were not built, unrelated).
  • python tools/check_conventions.py --base upstream/main: passes.
  • Not tested: Linux, macOS, a console, a title run. lstat, access, chmod, truncate, utimes, readlink, fopen and opendir still answer ENOENT or EINVAL for file/ on Windows; they are left for follow-ups. Earlier attempts: fix(libkernel): fail unlink of a file named with a trailing slash on Windows #5165, fix(libkernel): fail open of a file named with a trailing slash on Windows #5266 (closed).

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw; no silent stubs added
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • AI-assisted: yes

AI-assisted (Claude Code).

🤖 Generated with Claude Code

…with a trailing slash with ENOTDIR on Windows

FreeBSD 11.4 lookup answers ENOTDIR when a name with a trailing slash is
not a directory (sys/kern/vfs_cache.c:2104-2105). On Windows the guest path
of a mount alias lost the trailing slash, and these calls failed with ENOENT
or EINVAL. The resolver now keeps the slash on Windows too, and
sceKernelUnlink/unlink, remove, rename and sceKernelOpen/open answer
ENOTDIR. The trailing-slash checks in guest_filesystem, which only built
off Windows, run there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One related trailing-slash case appears to fall through: with file a regular file, NamesNonDirectoryWithSlash_nid_no_patch("file/child/") strips the final slash to file/child, where std::filesystem::status reports not-found plus an error. exists(status) is false, so the helper returns false even though an earlier component is a regular file; the Windows operation may then map that to ENOENT instead of ENOTDIR. HasNonDirectoryPrefix_nid_no_patch(native) already walks existing parent components and looks suitable as the fallback when the named path itself is not found. Could you add a nested case (at least open and one removal path) and use that fallback?

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/libs/prx/libc/src/General.cpp
core/libs/prx/libc/src/Rename.cpp
core/libs/prx/libkernel/File/src/Open.cpp
PR 6044 core/libs/tests/GuestFilesystem.cpp
PR 6226 core/libs/prx/libc/src/General.cpp
PR 6879 core/libs/prx/libc/src/General.cpp
PR 6936 core/libs/prx/libkernel/File/src/Open.cpp
PR 7121 core/libs/prx/libkernel/File/src/Open.cpp
PR 7201 core/libs/prx/libc/src/General.cpp
core/libs/prx/libc/src/Rename.cpp
core/libs/prx/libkernel/File/src/Open.cpp
PR 7530 core/libs/prx/libkernel/File/src/Open.cpp
core/libs/tests/GuestFilesystem.cpp
PR 8520 core/libs/prx/libkernel/File/src/Open.cpp
PR 8550 core/libs/tests/GuestFilesystem.cpp
PR 8658 core/libs/prx/libc/src/General.cpp
core/libs/prx/libc/src/Rename.cpp
core/libs/prx/libkernel/File/src/Open.cpp

file/child/ strips to file/child, which does not exist; unlink, remove,
open and sceKernelUnlink still answer ENOTDIR through the non-directory
prefix check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xXPBSXx

xXPBSXx commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I checked the nested case file/child/ (stripped to file/child, which does not exist). It already answers ENOTDIR for unlink, remove, open and sceKernelUnlink: the callers fall back to the non-directory-prefix check on ENOENT. I added these four cases to guest_filesystem (head 9eb680d). They pass on Windows (MinGW GCC 15.2, Release) with and without a HasNonDirectoryPrefix_nid_no_patch fallback inside NamesNonDirectoryWithSlash_nid_no_patch, so I did not add the fallback, since I could not show a failing case for it. If you have a call that still answers ENOENT for this shape, please name it and I will add it with a failing test. Not tested: rename with a nested name, Linux, macOS, a console.

AI-assisted (Claude Code).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for this. Since it changes Windows path resolution and we don't yet know of a title that needs it, I'm closing it for now, as with #5165/#5266. Please reopen with the title ID and the log line that shows the problem, and we'll pick it up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix error Closes an issue (Fixes #N) libc libkernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

libkernel: unlink, remove, rename and open fail with ENOENT or EINVAL instead of ENOTDIR for a regular file named with a trailing slash on Windows

3 participants